package engine;

import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.image.BufferStrategy;
import java.io.FileNotFoundException;
import java.io.PrintStream;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;
import javax.swing.JFrame;
import file.Logger;
import file.PrintStreamList;

/**
 * Provides a simple way to draw images or text to the screen, and basic mouse/keyboard input.
 * <p>
 * Some implementation details:
 * <li>MouseEvents received from GameEngine are translated to correspond to the usable area of the
 * window. This means that the top left usable pixel in the window is (0,0) for both drawing and
 * mouse events.
 * <li>The width and height of the window do not include the borders. A width of 10 by 10 ensures
 * that there are 100 pixels available for rendering.
 * <li>Input is blocking. This means that when you try to get input, but there is none, the game
 * will pause and wait until the user gives it more input.
 * 
 * @author Kyle_Solo
 */
public abstract class GameEngine {
	final class Display {
		private JFrame frame = null;
		private int height;
		private int width;
		private int xoff;
		private int yoff;

		public Display(int width, int height) {
			resize(width, height);
		}

		/**
		 * This method causes the display to render a single frame.
		 */
		public void draw() {
			BufferStrategy strategy = frame.getBufferStrategy();
			do {
				// The following loop ensures that the contents of the drawing
				// buffer
				// are consistent in case the underlying surface was recreated
				do {
					// Get a new graphics context every time through the loop
					// to make sure the strategy is validated
					Graphics graphics = strategy.getDrawGraphics();
					// Render to graphics2d
					Graphics2D g2d = (Graphics2D) graphics;
					g2d.translate(xoff, yoff);
					g2d.clipRect(0, 0, width, height);

					// call the render method so the user can draw their game
					render(g2d);

					// Dispose of the graphics context
					graphics.dispose();
					g2d.dispose();

					// Repeat the rendering if the drawing buffer contents
					// were restored
				} while (strategy.contentsRestored());

				// Display the buffer
				strategy.show();

				// Repeat the rendering if the drawing buffer was lost
			} while (strategy.contentsLost());
		}

		public int getHeight() {
			return height;
		}

		public int getWidth() {
			return width;
		}

		private void registerInput(Input input) {
			frame.addMouseListener(input);
			frame.addMouseMotionListener(input);
			frame.addMouseWheelListener(input);
			frame.addKeyListener(input);
		}

		/**
		 * Resizes the display
		 * 
		 * @param width
		 *            the width of the usable drawing area
		 * @param height
		 *            the height of the usable drawing area
		 */
		public void resize(int width, int height) {
			if (frame != null) {
				frame.dispose();
			}
			frame = new JFrame();
			frame.setVisible(true);
			this.width = width;
			this.height = height;
			Insets insets = frame.getInsets();
			xoff = insets.left;
			yoff = insets.top;
			frame.setSize(width + insets.left + insets.right, height + insets.top + insets.bottom);
			frame.setLocationRelativeTo(null);
			frame.createBufferStrategy(2);
			frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
			frame.setIgnoreRepaint(true);
		}
	}

	private final static class Input implements KeyListener, MouseListener, MouseMotionListener,
			MouseWheelListener {
		private final Display display;
		private BlockingDeque<InputEvent> inputQueue = new LinkedBlockingDeque<InputEvent>();

		public Input(Display display) {
			this.display = display;
			display.registerInput(this);
		}

		/**
		 * Gets the next input from either mouse or keyboard. If there is no input in the queue,
		 * blocks until there is.
		 * 
		 * @return the next input from the mouse or keyboard
		 */
		public InputEvent getInput() {
			return inputQueue.poll();
		}

		@Override
		public void keyPressed(KeyEvent arg0) {
			// inputQueue.add(arg0);
		}

		@Override
		public void keyReleased(KeyEvent arg0) {
			inputQueue.add(arg0);
		}

		@Override
		public void keyTyped(KeyEvent arg0) {
			// inputQueue.add(arg0);
		}

		@Override
		public void mouseClicked(MouseEvent arg0) {
			// arg0.translatePoint(-display.xoff, -display.yoff);
			// inputQueue.add(arg0);
		}

		@Override
		public void mouseDragged(MouseEvent arg0) {
			// arg0.translatePoint(-display.xoff, -display.yoff);
			// inputQueue.add(arg0);
		}

		@Override
		public void mouseEntered(MouseEvent arg0) {
			// arg0.translatePoint(-display.xoff, -display.yoff);
			// inputQueue.add(arg0);
		}

		@Override
		public void mouseExited(MouseEvent arg0) {
			// arg0.translatePoint(-display.xoff, -display.yoff);
			// inputQueue.add(arg0);
		}

		@Override
		public void mouseMoved(MouseEvent arg0) {
			arg0.translatePoint(-display.xoff, -display.yoff);
			inputQueue.add(arg0);
		}

		@Override
		public void mousePressed(MouseEvent arg0) {
			arg0.translatePoint(-display.xoff, -display.yoff);
			inputQueue.add(arg0);
		}

		@Override
		public void mouseReleased(MouseEvent arg0) {
			arg0.translatePoint(-display.xoff, -display.yoff);
			inputQueue.add(arg0);
		}

		// TODO: Decide what input events you want to receive
		@Override
		public void mouseWheelMoved(MouseWheelEvent arg0) {
			// arg0.translatePoint(-display.xoff, -display.yoff);
			// inputQueue.add(arg0);
		}
	}

	protected Display display;

	private final String ENGINE_VERSION = "0.001";

	protected Input input;

	private boolean logging = true;

	/**
	 * Draws a single frame to the screen.
	 */
	public final void draw() {
		display.draw();
	}

	/**
	 * Runs the game.
	 * 
	 * @return exit status
	 */
	public abstract int gameLoop();

	/**
	 * Gets the next input from either mouse or keyboard. If there is no input in the queue, blocks
	 * until there is.
	 * 
	 * @return the next input from the mouse or keyboard
	 */
	public InputEvent getInput() {
		return input.getInput();
	}

	/**
	 * This method is called by GameEngine's draw method. Draw your game on the provided Graphics2D
	 * object.
	 * 
	 * @param g2d
	 */
	public abstract void render(Graphics2D g2d);

	protected final void run(String[] args, int width, int height) {
		// TODO: Process command line arguments here
		for (String s : args) {
			if (s.equalsIgnoreCase("nologs")) {
				logging = false;
			}
		}

		if (logging) {
			// set up logging
			PrintStreamList log = new PrintStreamList(System.out);

			try {
				PrintStream logger = new Logger();
				log.add(logger);
				System.setOut(log);
				System.out.print("-------------------------------------------------------\n");
				System.out.println("Logging Started");
			} catch (FileNotFoundException e) {
				System.out.println("Log file could not be created: FileNotFoundException");
			}
		}

		// print the version
		System.out.println("Initializing engine version " + ENGINE_VERSION);

		// are assertions enabled?
		boolean hasAssertions = false;
		assert hasAssertions = true;
		if (hasAssertions) {
			System.out.println("Assertions are enabled");
		} else {
			System.out.println("Assertions are disabled");
		}

		System.out.println("Setting up the display...");
		display = new Display(width, height);
		System.out.println("Setting up the input handler...");
		input = new Input(display);
		System.out.println("Initialization Complete");
		System.out.println("Calling gameLoop();");
		int exitStatus = gameLoop();
		System.out.println("gameLoop() terminated with exit status " + exitStatus);
		display.frame.dispose();
	}
}